<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <title>使用tun虚拟网络接口建立IP隧道的实例 - whowin - 发表我个人原创作品的技术博客</title>
  <meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>

<meta http-equiv="Cache-Control" content="no-transform" />
<meta http-equiv="Cache-Control" content="no-siteapp" />

<meta name="theme-color" content="#f8f5ec" />
<meta name="msapplication-navbutton-color" content="#f8f5ec">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="#f8f5ec">


<meta name="author" content="whowin" /><meta name="description" content="通常的socket编程，面对的都是物理网卡，Linux下其实很容易创建虚拟网卡；本文简单介绍一下Linux虚拟网卡的概念，并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡，最终实现一个从客户端到服务器的简单的IP隧道，希望本文能对理解虚拟网卡和IP隧道有所帮助，本文将提供完整的源程序；阅读本文需要具备在Linux下使用C语言进行IPv4下socket编程的基本能力，本文对网络编程的初学者难度较大。
" /><meta name="keywords" content="linux, socket, hugo, dos" />






<meta name="generator" content="Hugo 0.97.3 with theme even" />


<link rel="canonical" href="https://whowin.gitee.io/post/blog/network/0018-tun-example-for-setting-up-ip-tunnel/" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">



<link href="/sass/main.min.e3fea119b1980e848b03dffbeddb11dd0fba483eed0e5f11870fb8e31f145bbd.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.1.20/dist/jquery.fancybox.min.css" integrity="sha256-7TyXnr2YU040zfSP+rEcz29ggW4j56/ujTPwjMzyqFY=" crossorigin="anonymous">


<meta property="og:title" content="使用tun虚拟网络接口建立IP隧道的实例" />
<meta property="og:description" content="通常的socket编程，面对的都是物理网卡，Linux下其实很容易创建虚拟网卡；本文简单介绍一下Linux虚拟网卡的概念，并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡，最终实现一个从客户端到服务器的简单的IP隧道，希望本文能对理解虚拟网卡和IP隧道有所帮助，本文将提供完整的源程序；阅读本文需要具备在Linux下使用C语言进行IPv4下socket编程的基本能力，本文对网络编程的初学者难度较大。" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://whowin.gitee.io/post/blog/network/0018-tun-example-for-setting-up-ip-tunnel/" /><meta property="article:section" content="post" />
<meta property="article:published_time" content="2023-03-07T16:43:29+08:00" />
<meta property="article:modified_time" content="2023-03-07T16:43:29+08:00" />

<meta itemprop="name" content="使用tun虚拟网络接口建立IP隧道的实例">
<meta itemprop="description" content="通常的socket编程，面对的都是物理网卡，Linux下其实很容易创建虚拟网卡；本文简单介绍一下Linux虚拟网卡的概念，并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡，最终实现一个从客户端到服务器的简单的IP隧道，希望本文能对理解虚拟网卡和IP隧道有所帮助，本文将提供完整的源程序；阅读本文需要具备在Linux下使用C语言进行IPv4下socket编程的基本能力，本文对网络编程的初学者难度较大。"><meta itemprop="datePublished" content="2023-03-07T16:43:29+08:00" />
<meta itemprop="dateModified" content="2023-03-07T16:43:29+08:00" />
<meta itemprop="wordCount" content="6958">
<meta itemprop="keywords" content="Linux,网络编程,IP tunnel,IP隧道," /><meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="使用tun虚拟网络接口建立IP隧道的实例"/>
<meta name="twitter:description" content="通常的socket编程，面对的都是物理网卡，Linux下其实很容易创建虚拟网卡；本文简单介绍一下Linux虚拟网卡的概念，并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡，最终实现一个从客户端到服务器的简单的IP隧道，希望本文能对理解虚拟网卡和IP隧道有所帮助，本文将提供完整的源程序；阅读本文需要具备在Linux下使用C语言进行IPv4下socket编程的基本能力，本文对网络编程的初学者难度较大。"/>

<!--[if lte IE 9]>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/classlist/1.1.20170427/classList.min.js"></script>
<![endif]-->

<!--[if lt IE 9]>
  <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
<![endif]-->

  <script async src="/js/busuanzi.pure.mini.js"></script><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-9724909319263152"
     crossorigin="anonymous"></script>


</head>
<body>
  <div id="mobile-navbar" class="mobile-navbar">
  <div class="mobile-header-logo">
    <a href="/" class="logo">WhoWin</a>
  </div>
  <div class="mobile-navbar-icon">
    <span></span>
    <span></span>
    <span></span>
  </div>
</div>
<nav id="mobile-menu" class="mobile-menu slideout-menu">
  <ul class="mobile-menu-list">
    <a href="/">
        <li class="mobile-menu-item">首页</li>
      </a><a href="/post/">
        <li class="mobile-menu-item">文章归档</li>
      </a><a href="/article-categories/categories/">
        <li class="mobile-menu-item">文章分类</li>
      </a><a href="/tags/">
        <li class="mobile-menu-item">文章标签</li>
      </a><a href="/about/about/">
        <li class="mobile-menu-item">关于</li>
      </a>
  </ul>

  


</nav>

  <div class="container" id="mobile-panel">
    <header id="header" class="header">
        <div class="logo-wrapper">
  <a href="/" class="logo">WhoWin</a>
  
  <div style="position:absolute; left: 80px; top: 75px; color: crimson">
      ———开源和分享是技术发展的源泉和动力；本博客所有文章均为原创
  </div>
</div>





<nav class="site-navbar">
  <ul id="menu" class="menu">
    <li class="menu-item">
        <a class="menu-item-link" href="/">首页</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/post/">文章归档</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/article-categories/categories/">文章分类</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/tags/">文章标签</a>
      </li><li class="menu-item">
        <a class="menu-item-link" href="/about/about/">关于</a>
      </li>
  </ul>
</nav>

    </header>

    <main id="main" class="main">
      <div class="content-wrapper">
        <div id="content" class="content">
          <article class="post">
    
    <header class="post-header">
      <h1 class="post-title">使用tun虚拟网络接口建立IP隧道的实例</h1>

      <div class="post-meta">
        <span class="post-time"> 2023-03-07 </span>
        <div class="post-category">
            <a href="/categories/linux/"> Linux </a>
            <a href="/categories/c-language/"> C Language </a>
            <a href="/categories/network/"> Network </a>
            </div>
        
      </div>
    </header>

    <div class="post-toc" id="post-toc">
  <h2 class="post-toc-title">文章目录</h2>
  <div class="post-toc-content always-active">
    <nav id="TableOfContents">
  <ul>
    <li>
      <ul>
        <li><a href="#1-linux下的虚拟网卡tuntap">1. Linux下的虚拟网卡TUN/TAP</a></li>
        <li><a href="#2-构建一个tun设备">2. 构建一个TUN设备</a></li>
        <li><a href="#3-使用tun设备的基本数据流向">3. 使用tun设备的基本数据流向</a></li>
        <li><a href="#4-使用tun设备搭建一个简单的ip隧道">4. 使用tun设备搭建一个简单的IP隧道</a></li>
        <li><a href="#5-后记">5. 后记</a></li>
        <li><a href="#欢迎订阅-网络编程专栏httpsblogcsdnnetwhowincategory_12180345html"><strong>欢迎订阅 <a href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></a></li>
      </ul>
    </li>
  </ul>
</nav>
  </div>
</div>
    <div class="post-content">
      <p>通常的socket编程，面对的都是物理网卡，Linux下其实很容易创建虚拟网卡；本文简单介绍一下Linux虚拟网卡的概念，并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡，最终实现一个从客户端到服务器的简单的IP隧道，希望本文能对理解虚拟网卡和IP隧道有所帮助，本文将提供完整的源程序；阅读本文需要具备在Linux下使用C语言进行IPv4下socket编程的基本能力，本文对网络编程的初学者难度较大。</p>
<h2 id="1-linux下的虚拟网卡tuntap">1. Linux下的虚拟网卡TUN/TAP</h2>
<ul>
<li>TUN和TAP是Linuxn内核的虚拟网络设备，不同于普通靠硬件网络适配器实现的设备，这些虚拟的网络设备全部用软件实现，并可以向运行于Linux上的应用软件提供与硬件的网络设备完全相同的功能；</li>
<li>TAP等同于一个以太网设备，它操作OSI模型的第二层(数据链路层)数据包，通常我们所使用的网络就是以太网数据帧，所以要使用TAP设备，就需要自己构建以太网报头、IP报头、TCP/UDP报头；</li>
<li>TUN模拟了网络层设备，操作第三层(网络层)数据包，通常我们使用的TCP/UDP报文在网络层使用的IP协议，所以使用TUN设备，需要自己构建IP报头和TCP/UDP报头，比TAP设备少构建一个以太网报头；</li>
<li>Linux通过TUN/TAP设备向绑定该设备的用户空间的应用程序发送数据；同样，用户空间的应用程序也可以像操作硬件网络设备那样，通过TUN/TAP设备发送数据；在后面这种情况下，TUN/TAP设备向Linux的网络协议栈提交数据包，从而模拟从外部接收数据的过程；</li>
</ul>
<h2 id="2-构建一个tun设备">2. 构建一个TUN设备</h2>
<ul>
<li>
<p>上一节的描述显然过于枯燥，可能会对初次接触虚拟网卡的读者感到困惑，不知所云，本节将实际建立一个tun设备，帮助你走出困惑；</p>
</li>
<li>
<p>构建一个基本的tun设备，只需要两个步骤</p>
<ol>
<li><strong>编写一个程序，至少完成三个任务</strong>
<ul>
<li>
<p>以可读写模式打开设备文件 <code>/dev/net/tun</code></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">fd</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">&#34;/dev/net/tun&#34;</span><span class="p">,</span> <span class="n">O_RDWR</span><span class="p">));</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>向Linux内核注册一个tun设备名称，本例中为tun0</p>
<blockquote>
<p>(struct ifreq)定义在头文件&lt;linux/if.h&gt;中，在我的很多文章中都有介绍，比如文章<a href="https://blog.csdn.net/whowin/article/details/128891255">《如何使用raw socket发送UDP报文》</a>，如果需要，可以参考；</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="k">struct</span> <span class="n">ifreq</span> <span class="n">ifr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">ifr</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">ifr</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="n">ifr</span><span class="p">.</span><span class="n">ifr_flags</span> <span class="o">=</span> <span class="n">IFF_TUN</span> <span class="o">|</span> <span class="n">IFF_NO_PI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">strcpy</span><span class="p">(</span><span class="n">ifr</span><span class="p">.</span><span class="n">ifr_name</span><span class="p">,</span> <span class="s">&#34;tun0&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ioctl</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">TUNSETIFF</span><span class="p">,</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">ifr</span><span class="p">);</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>编写处理tun0接收/发送数据的程序</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-C" data-lang="C"><span class="line"><span class="cl"><span class="kt">char</span> <span class="n">buffer</span><span class="p">[</span><span class="n">BUFSIZE</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">read</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">buffer</span><span class="p">,</span> <span class="n">BUFSIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// todo
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ul>
</li>
<li><strong>为设备分配IP地址</strong>(本例中为tun0分配的IP为10.0.0.1)
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ifconfig tun0 10.0.0.1 netmask 255.255.255.0 up
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ol>
</li>
<li>
<p>把上面的代码片段组合在一起，就可以完成一个tun设备的建立，文件名：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/tun-01.c">tun_01.c</a>(<strong>点击文件名下载源文件</strong>)</p>
</li>
<li>
<p>这段程序在进入循环前增加了 <code>system(&quot;ifconfig tun0 10.0.0.1/24 up&quot;)</code>，为tun0分配了IP地址10.0.0.1，所以运行完后就不需要再为这个设备分配IP了；</p>
</li>
<li>
<p>编译：<code>gcc -Wall tun-01.c -o tun-01</code></p>
</li>
<li>
<p>该程序需要root权限运行，主要是因为其中使用了ioctl，运行：<code>sudo ./tun-01</code></p>
</li>
<li>
<p>运行该程序，会构建一个tun设备，打开一个新的终端，使用 <code>ifconfig</code> 将可以看到系统中多了一个虚拟网络接口tun0，使用 <code>route -n</code> 查看路由也会看到增加了一条关于tun0设备的路由</p>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-of-setting-up-tun0.png" alt="screenshot of setting up tun0 device"></p>
<center><b>图1：构建一个tun设备后</b></center>
<hr>
</li>
<li>
<p>尽管建立起了虚拟网卡tun0，但因为程序过于简单，所以这样建立的设备什么事情都做不了，必须完善程序，才能让这个设备真正地发挥作用；</p>
</li>
<li>
<p>tun设备是一个第三层(网络层)的设备，在这个设备上只能收到IP报头，收不到以太网报头，所以Linux索性没有为tun设备分配MAC地址；</p>
</li>
<li>
<p>后面将以本节的程序为基础，不断改进，最终写出一个简单的IP隧道的程序。</p>
</li>
</ul>
<h2 id="3-使用tun设备的基本数据流向">3. 使用tun设备的基本数据流向</h2>
<ul>
<li>
<p>设备建立起来以后，程序员关心的是我们如何从这个设备上收发报文，如何处理这些报文；</p>
</li>
<li>
<p>对于一个物理网络接口而言，接口一端连接着网络协议栈，另一端连接着物理网络；而对于一个虚拟网络接口而言，接口的一端仍然连接着网络协议栈，但是另一端连接着一个应用程序，也就是我们前面下载的那个程序(<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/tun-01.c">tun-01.c</a>)，我们把这个程序称为 <strong>application-tun</strong>；</p>
</li>
<li>
<p>可以和一个物理网络接口比较来说明虚拟网络接口的数据流向，在物理接口上要发送到物理网络上去的报文，相对于虚拟接口将被发送到应用程序 <strong>application-tun</strong> 上；</p>
</li>
<li>
<p>当我们使用socket发送报文时，报文被提交给Linux的网络协议栈，协议栈为报文封装各个协议层的报头，并根据路由表将报文交给相应设备的驱动程序，比如enp0s3的驱动程序，然后由驱动程序将报文发送到物理网络上(物理设备)，或者发送给应用程序 application-tun(虚拟设备)；</p>
</li>
<li>
<p>在上一节中，我们使用 <code>route -n</code> 已经看到了关于tun0设备的路由：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">内核 IP 路由表
</span></span><span class="line"><span class="cl">目标            网关            子网掩码        标志  跃点   引用  使用 接口
</span></span><span class="line"><span class="cl">0.0.0.0        192.168.2.3     0.0.0.0        UG   100    0     0  enp0s3
</span></span><span class="line"><span class="cl">10.0.0.0       0.0.0.0         255.255.255.0  U    0      0     0  tun0
</span></span><span class="line"><span class="cl">169.254.0.0    0.0.0.0         255.255.0.0    U    1000   0     0  enp0s3
</span></span><span class="line"><span class="cl">192.168.2.0    0.0.0.0         255.255.255.0  U    100    0     0  enp0s3
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>路由表明，当目的IP地址为10.0.0.x时，报文将被送到虚拟设备tun0的驱动程序上去，该设备绑定的IP为10.0.0.1；</p>
</li>
<li>
<p>还有一条路由，当目的IP地址为192.168.2.x时，报文将被送到物理设备enp0s3的驱动程序上去，该设备绑定的IP为192.168.2.114；</p>
</li>
<li>
<p>这两条路由比较相似，区别是一个是物理设备enp0s3，另一个是虚拟设备tun0，我们拿这两条路由进行对比说明数据流向；</p>
</li>
<li>
<p><strong>发送报文到物理/虚拟接口绑定的IP地址上</strong></p>
<ul>
<li>当我们发送一个UDP报文到 <strong>192.168.2.114:5678</strong>(也就是本机物理设备enp0s3的IP)时，根据路由，报文被送给enp0s3的驱动程序，驱动程序并不会把这个报文发送到物理网络上，因为enp0s3的驱动程序已经是这个报文最终的目的地，所以enp0s3的驱动程序会将这个报文发到一个正在监听192.168.2.114:5678的用户程序上，如果我们没有编写这个程序，报文将被丢弃，这样我们就收不到这个报文；</li>
<li>当我们发送一个UDP报文到 <strong>10.0.0.1:5678</strong>(也就是本机虚拟设备tun0的IP)时，根据路由，报文被送给tun0的驱动程序，驱动程序并不会把这个报文发送到 application-tun 上，因为tun0的驱动程序已经是这个报文的最终目的地，所以tun0的驱动程序会将这个报文发到一个正在监听10.0.0.1:5678的用户程序上，和物理设备一样，如果我们没有编写这个程序，报文将被丢弃，我们收不到这个报文；</li>
</ul>
<p><img src="https://whowin.gitee.io/images/180018/send-data-tun-local.png" alt="send data to tun&amp;rsquo;s IP"></p>
<center><b>图2：发送报文到tun0的IP上</b></center>
</li>
</ul>
<hr>
<ul>
<li>
<p><strong>发送报文到符合路由的其他IP地址上</strong></p>
<ul>
<li>当我们发送一个UDP报文到 <strong>192.168.2.112:5678</strong> 时，根据路由报文会被送给enp0s3的驱动程序，驱动程序会把这个报文发送到物理网络上；</li>
<li>当我们发送一个UDP报文到 <strong>10.0.0.2:5678</strong> 时，根据路由报文会被送给报文被送给tun0的驱动程序，驱动程序会把这个报文发送到应用程序 <strong>application-tun</strong> 上；</li>
</ul>
<p><img src="https://whowin.gitee.io/images/180018/send-data-tun-remote.png" alt="send data to tun&amp;rsquo;s route"></p>
<center><b>图3：发送报文到符合tun0路由的其他IP上</b></center>
</li>
</ul>
<hr>
<ul>
<li>
<p><strong>对上述说明可以做一个简单的测试</strong></p>
<ul>
<li>打开终端，运行前面的程序：tun-01
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ./tun-01
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>打开另一个终端，使用下面命令分别向 <strong>10.0.0.1:5678</strong> 发送数据，在运行 tun-01 的终端上并不会显示收到数据；
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;hello&#34;</span> &gt; /dev/udp/10.0.0.1/5678
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>使用下面命令分别向 <strong>10.0.0.2:5678</strong> 发送数据，在运行 tun-01 的终端上会显示收到数据；
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;hello&#34;</span> &gt; /dev/udp/10.0.0.2/5678
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ul>
</li>
<li>
<p><strong>源IP地址的选择</strong></p>
<ul>
<li>当我们在电脑系统上运行 <code>sudo ./tun-01</code> 时，我们的系统就有了两个IP地址，一个是物理网卡的，IP为192.168.2.114，另一个是虚拟网卡的，IP为10.0.0.1；</li>
<li>当我们在做上面的测试时，我们用 <code>echo ......</code> 命令向 10.0.0.1 和 10.0.0.2 发送了UDP消息，发送时我们并没有指定源IP地址，那么发出的消息的源IP地址是什么呢？192.168.2.114 还是 10.0.0.1？</li>
<li>我们把前面那个程序 tun_01.c 改一下，一是增加一些错误判断，使这个程序更加完善一些，另外我们增加一个显示IPv4报头的功能，这样我们就可以看到IP头中的源IP地址了；</li>
<li>改好的程序文件名为：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/tun-02.c">tun-02.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li>编译：<code>gcc -Wall tun-02.c -o tun-02</code></li>
<li>下面我们做个测试，向 10.0.0.2:5678 发送一条UDP消息，我们看看源IP地址是什么？
<ul>
<li>
<p>打开一个终端，运行tun-02</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ./tun-02
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>打开另一个终端，向10.0.0.2发送消息</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;hello&#34;</span> &gt; /dev/udp/10.0.0.2/5678
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>在运行tun-02的终端上显示出源IP地址为10.0.0.1</p>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-for-source-ip.png" alt="screenshot for source IP"></p>
<center><b>图4：Linux在多网卡环境下选择源IP</b></center>
</li>
</ul>
</li>
</ul>
<hr>
<ul>
<li>
<p>当使用sendmsg()发送数据时，是可以显式地指定源IP地址的；</p>
</li>
<li>
<p>路由表中有一个src字段，当没有指定源IP地址时，将使用选定路由的src字段作为源IP地址，使用 <code>ip route</code> 可以看到src字段</p>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-of-ip-route.png" alt="screenshot of &amp;lsquo;ip route&amp;rsquo;"></p>
<center><b>图5：ip route命令显示路由表中src字段</b></center>
</li>
</ul>
<hr>
<ul>
<li>如果选定的路由没有src字段，Linux会搜寻选定路由的网络接口上所有绑定的IP，对IPv6将选择第一个搜寻到的地址，对IPv4则尽量选择与目标IP在同一网段的IP地址；</li>
</ul>
</li>
</ul>
<h2 id="4-使用tun设备搭建一个简单的ip隧道">4. 使用tun设备搭建一个简单的IP隧道</h2>
<ul>
<li>
<p>tun实际上是tunnel的前面三个字母，tun设备注定和隧道是有关系的，tun设备也的确常用来构建一个IP隧道；</p>
</li>
<li>
<p>IP报文其实是指：IP报头 + TCP/UDP报头 + 数据</p>
</li>
<li>
<p>所谓IP隧道是指把一个IP报文作为数据再封装一个TCP头和IP头，所以整个报文变成：IP报头 + TCP报头 + (IP报头 + TCP/UDP报头 + 数据)</p>
</li>
<li>
<p>至于IP隧道的意义、应用场景之类的，本文不予讨论，可以自己去百个度或者谷个歌查一下，本文将致力于做一个简单的IP隧道；</p>
</li>
<li>
<p>先看一张示意图</p>
<p><img src="https://whowin.gitee.io/images/180018/simple-ip-tunnel.png" alt="Diagram Simple IP tunnel"></p>
<center><b>图6：简单的IP隧道示意图</b></center>
</li>
<li>
<p>有两台电脑，Computer A和Computer B</p>
<ul>
<li>Computer A：
<ol>
<li>物理网卡为enp0s3，绑定IP：192.168.2.112</li>
<li>虚拟网卡为tun0，绑定IP：10.0.0.2</li>
</ol>
</li>
<li>Computer B：
<ol>
<li>物理网卡为enp0s3，绑定IP：192.168.2.114</li>
<li>虚拟网卡为tun0，绑定IP：10.0.0.1</li>
</ol>
</li>
</ul>
</li>
<li>
<p>Computer A和Computer B的路由表一样，如下：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">内核 IP 路由表
</span></span><span class="line"><span class="cl">目标            网关            子网掩码        标志  跃点   引用  使用 接口
</span></span><span class="line"><span class="cl">0.0.0.0        192.168.2.3     0.0.0.0        UG   100    0     0  enp0s3
</span></span><span class="line"><span class="cl">10.0.0.0       0.0.0.0         255.255.255.0  U    0      0     0  tun0
</span></span><span class="line"><span class="cl">169.254.0.0    0.0.0.0         255.255.0.0    U    1000   0     0  enp0s3
</span></span><span class="line"><span class="cl">192.168.2.0    0.0.0.0         255.255.255.0  U    100    0     0  enp0s3
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>Computer A的应用程序app A向10.0.0.1:1234发送报文，Computer B的应用程序app D侦听在10.0.0.1:1234上；</p>
</li>
<li>
<p>目标很简单，computer A的app A直接向10.0.0.1:1234发送报文，computer B的app D能够正常收到收到，就像在一个局域网上一样；</p>
</li>
<li>
<p>首先要明确的，物理局域网的网段是192.168.2.x，所以向10.0.0.1发送报文并不会被送到物理局域网上，按照路由，这条报文会被送到tun0的驱动程序上去，因为10.0.0.1并不是computer A的虚拟网卡tun0绑定的IP，所以驱动程序会把这个报文送到application-tun上，所以如果我们不做处理，这个报文根本无法到达目的地；</p>
</li>
<li>
<p>如何处理这个报文使其发送到computer B的app D上去呢？通常的方法就是在computer A和computer B的物理网卡之间建立一条IP隧道；</p>
</li>
<li>
<p>当computer A启动applition-tun时，主动发起向computer B的连接，端口号定为5678，computer B在启动applition-tun时，主动侦听在端口5678上，并等待computer A的连接请求，一旦连接建立，这个隧道就建好了；</p>
</li>
<li>
<p>computer A的application-tun收到发往10.0.0.1的报文时，要在整个IP报文上再包装上一个IP报头+TCP报头，TCP报头中指定目的端口号为5678，IP报头中指定目的IP为192.168.2.114，源IP为192.168.2.112，然后把这个新报文从建立的隧道中发出；</p>
</li>
<li>
<p>computer B上侦听在5678端口上的应用程序app C会收到这个报文，app C去掉IP报头和TCP报头，把数据部分作为一个完整的报文重新从socket发出，这个报文的内容正是computer A发出的原始报文，computer B的内核协议栈根据路由会将该报文发给tun0的驱动程序，驱动程序会将这个报文送到正在侦听1234端口的app D上；</p>
</li>
<li>
<p>在客户端(computer A)需要编写一个程序，程序文件名：app-client.c，这个程序应遵循以下处理流程：</p>
<ol>
<li>打开 /dev/net/tun 文件，返回tun_fd，在内核注册虚拟设备 tun0；</li>
<li>创建socket，sock_fd，在这个 sock_fd 上连接服务器端(computer B)的5678端口，建立IP隧道；</li>
<li>使用select检查tun_fd和sock_fd，并分别处理在这两个 fd 上收到的数据；</li>
<li>在tun_fd上收到数据的处理流程
<ul>
<li>将收到的包括IP报头在内的报文作为数据从sock_fd上发出</li>
</ul>
</li>
<li>在sock_fd上收到数据的处理流程
<ul>
<li>把收到的数据作为一个IP报文显示报头及内容</li>
</ul>
</li>
</ol>
</li>
<li>
<p>在服务器端(computer B)编写一个程序，文件名为：app-server.c，这个程序应遵循以下处理流程：</p>
<ol>
<li>打开 /dev/net/tun 文件，返回tun_fd，在内核注册虚拟设备 tun0；</li>
<li>创建socket，fd为sock_fd，在这个 sock_fd 上侦听5678端口，等待客户端连接以建立IP隧道；</li>
<li>接受客户端的连接请求，为新连接创建socket，fd为net_fd</li>
<li>使用select检查tun_fd和net_fd，并分别处理在这两个 fd 上收到的数据；</li>
<li>在tun_fd上收到数据的处理流程
<ul>
<li>将收到的包括IP报头在内的报文作为数据从net_fd上发出</li>
</ul>
</li>
<li>在net_fd上收到数据的处理流程
<ul>
<li>把收到的数据(不包括IP报头和TCP报头)作为带有IP报头的报文发到tun_fd上</li>
</ul>
</li>
</ol>
</li>
<li>
<p>客户端程序：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/app-client.c">app-client.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li>
<p>客户端程序编译：<code>gcc -Wall app-client.c -o app-client</code></p>
</li>
<li>
<p>服务器端程序：<a href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/app-server.c">app-server.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li>
<p>服务器端程序编译：<code>gcc -Wall app-server.c -o app-server</code></p>
</li>
<li>
<p>为了运行方便，也可以将这两个程序写成守护进程，将程序中注释掉的 <code>daemon(0, 0)</code> 放开即可；</p>
</li>
<li>
<p>请根据实际情况调整程序中的宏定义，SERVER_IP和TUN_IP；</p>
</li>
<li>
<p>在服务器端注意防火墙设置，打开5678端口或者关闭防火墙；</p>
</li>
<li>
<p>这两个程序的运行均需要root权限。</p>
</li>
<li>
<p>客户端程序测试</p>
<ul>
<li>
<p>需要打开三个终端窗口；</p>
</li>
<li>
<p>首先将程序中的SERVER_IP改为本机的IP地址，然后重新编译；</p>
</li>
<li>
<p>打开第一个终端，运行 <code>nc -l 5678</code>，这个命令将监听本机的5678端口；</p>
</li>
<li>
<p>打开第二个终端，运行客户端程序：<code>sudo ./app-client</code>，应该显示&quot;Connected to server &hellip;&quot;</p>
</li>
<li>
<p>打开第三个终端，运行 <code>echo &quot;hello&quot; &gt; /dev/udp/10.0.0.1/1234</code>，这个命令将向10.0.0.1的1234端口发送一个UDP报文，报文的数据部分为&quot;hello&quot;</p>
</li>
<li>
<p>此时在第二个终端上应该显示&quot;Received data from tun&quot;，在第一个终端上收到一些乱码，但其中有&quot;hello&quot;字符串，乱码是因为我们收到的数据包括IP报头和UDP报头，这两部分是二进制的数据；</p>
</li>
<li>
<p>如果你看到的和上面的描述一致，那么你的客户端程序基本没有问题；</p>
</li>
<li>
<p>下面是截屏</p>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-of-1st-termianl-for-client.png" alt="screenshot of 1st terminal for client test"></p>
<center><b>图7：测试客户端程序时的第一个终端</b></center>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-of-2nd-terminal-for-client.png" alt="screenshot of 2nd terminal for client test"></p>
<center><b>图8：测试客户端程序时的第二个终端</b></center>
</li>
</ul>
</li>
<li>
<p>服务器端程序无需独立测试；</p>
</li>
<li>
<p>IP隧道测试</p>
<ul>
<li>
<p>需要两台机器，一台做客户端，另一台做服务器端</p>
</li>
<li>
<p>再次强调，请根据实际情况调整程序中的宏定义，SERVER_IP和TUN_IP，并重新编译程序；</p>
</li>
<li>
<p>在服务器端注意防火墙设置，打开5678端口或者关闭防火墙；</p>
</li>
<li>
<p>在服务器端和客户端均需要打开两个终端，下面是测试方法示意图</p>
<p><img src="https://whowin.gitee.io/images/180018/diagram-for-testing.png" alt="diagram for testing"></p>
<center><b>图9：测试示意图</b></center>
</li>
<li>
<p>在服务器第一个终端上启动服务器端程序 <code>sudo ./app-server</code></p>
</li>
<li>
<p>在服务器第二个终端上执行命令 <code>nc -luk 1234</code>，这个命令将一直监听在UDP的1234端口上；</p>
</li>
<li>
<p>在客户端第一个终端上启动客户端程序 <code>sudo ./app-client</code></p>
</li>
<li>
<p>在客户端第二个终端上执行命令 <code>echo &quot;hello&quot; &gt; /dev/udp/10.0.0.1/1234</code>，这个命令将向10.0.0.1(服务器端的tun0绑定的IP)的UDP端口1234发送一条消息</p>
</li>
<li>
<p>客户端第二个终端上向10.0.0.1:1234发送了一个UDP消息，内容是：hello</p>
</li>
<li>
<p>最终在服务器端的第二个终端上收到了这个信息</p>
</li>
<li>
<p>下面是运行截图</p>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-server-1st-terminal-for-testing.png" alt="screenshot server 1st terminal for testing"></p>
<center><b>图10：服务器端第一个终端</b></center>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-client-1st-terminal-for-testing.png" alt="screenshot client 1st terminal for testing"></p>
<center><b>图11：客户端第一个终端</b></center>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-server-2nd-terminal-for-testing.png" alt="screenshot server 2nd terminal for testing"></p>
<center><b>图12：服务器端第二个终端</b></center>
<p><img src="https://whowin.gitee.io/images/180018/screenshot-client-2nd-terminal-for-testing.png" alt="screenshot client 2nd terminal for testing"></p>
<center><b>图13：客户端第二个终端</b></center>
</li>
</ul>
</li>
</ul>
<h2 id="5-后记">5. 后记</h2>
<ul>
<li>我们实现了一个简单的IP隧道，在这个IP隧道，我们传送一个UDP报文，我们传了一个UDP报文而不是一个TCP报文是为了省去connect()的麻烦；</li>
<li>这样一个IP隧道并不局限在局域网中，通过互联网一样可以建立一个IP隧道；</li>
<li>我们的这个服务器端的程序仅处理了一个客户端的连接，如果我们允许多个客户端接入并建立多条IP隧道，如果连接的多个客户端的tun都绑定在同一个网段上，那么通过服务器显然是可以像局域网一样相互通信的，好像多个终端在一个局域网里一样，是不是有点像<img src="https://whowin.gitee.io/images/180018/vpn.png" alt="">，实际上很多<img src="https://whowin.gitee.io/images/180018/vpn.png" alt="">就是使用IP隧道实现的；</li>
<li>IP隧道还可以用于很多场合，如果你的防火墙不允许某些协议通过，那么你可以通过一个防火墙允许的端口与服务器建立一个IP隧道，然后在这个IP隧道里跑那个不被防火墙允许的协议，就像我们在IP隧道里跑UDP协议一样；</li>
<li>建立隧道也不一定非得使用TCP/IP协议，比如可以使用ICMP协议建立一个ICMP隧道，当你的电脑只能ping通你的服务器，其它的所有协议都无法通过防火墙的情况下，使用ICMP协议建立一个ICMP隧道，然后可以在这个隧道里跑其它协议；</li>
<li>虚拟网络接口的用途很多，现在的虚拟机、容器等大多使用了虚拟网络接口，希望这篇文章可以让你对虚拟网络接口有个初步的认识。</li>
</ul>
<h2 id="欢迎订阅-网络编程专栏httpsblogcsdnnetwhowincategory_12180345html"><strong>欢迎订阅 <a href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr>
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: <a href="mailto:hengch@163.com">hengch@163.com</a></strong></p>
<p><img src="https://whowin.gitee.io/images/qrcode/sponsor-qrcode.png" alt="donation"></p>
<!--gitee
[article01]:https://whowin.gitee.io/post/blog/network/0006-send-udp-with-raw-socket/
-->
<!-- CSDN
[img01]:https://img-blog.csdnimg.cn/img_convert/ebd984e31848ac92b1bd298d5ee258f6.png
[img02]:https://img-blog.csdnimg.cn/img_convert/eed167269500cfa34a43ad1ef6f0cb64.png
[img03]:https://img-blog.csdnimg.cn/img_convert/9c950df68ff59f890ea6cee30256c435.png
[img04]:https://img-blog.csdnimg.cn/img_convert/7f9e2dc7debf88b9d95c2f85d73cf613.png
[img05]:https://img-blog.csdnimg.cn/img_convert/2d3418e11aac01bc1836abaf1956cb64.png
[img06]:https://img-blog.csdnimg.cn/img_convert/89fe2c83e7cd2143df4db797f0358593.png
[img07]:https://img-blog.csdnimg.cn/img_convert/ee49f7c1a2d5ae1e55ddd89c0cc02ec0.png
[img08]:https://img-blog.csdnimg.cn/img_convert/292a56ca58a4cab4da513c35af864c11.png
[img09]:https://img-blog.csdnimg.cn/img_convert/f51b997553ca8b386d5ca21d0ed89423.png
[img10]:https://img-blog.csdnimg.cn/img_convert/819d743886e012be90133bfe04be6570.png
[img11]:https://img-blog.csdnimg.cn/img_convert/d38d55545733ea3d26306d14f738adea.png
[img12]:https://img-blog.csdnimg.cn/img_convert/f02f8479a3e4dd55237c911acfa13375.png
[img13]:https://img-blog.csdnimg.cn/img_convert/0dd24d1c29423d37348b09498272e689.png
[img14]:https://img-blog.csdnimg.cn/img_convert/de785033e8b1079b8f5c92f5da8241ea.png
-->
    </div>

    <div class="post-copyright">
  <p class="copyright-item">
    <span class="item-title">文章作者</span>
    <span class="item-content">whowin</span>
  </p>
  <p class="copyright-item">
    <span class="item-title">上次更新</span>
    <span class="item-content">
        2023-03-07
        
    </span>
  </p>
  
  
</div>
<footer class="post-footer">
      <div class="post-tags">
          <a href="/tags/linux/">Linux</a>
          <a href="/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/">网络编程</a>
          <a href="/tags/ip-tunnel/">IP tunnel</a>
          <a href="/tags/ip%E9%9A%A7%E9%81%93/">IP隧道</a>
          </div>
      <nav class="post-nav">
        <a class="prev" href="/post/blog/network/0020-implement-ping-program-with-sock-dgram/">
            <i class="iconfont icon-left"></i>
            <span class="prev-text nav-default">使用SOCK_DGRAM类型的socket实现的ping程序</span>
            <span class="prev-text nav-mobile">上一篇</span>
          </a>
        <a class="next" href="/post/blog/network/0017-sync-time-from-time-server-using-sntp/">
            <span class="next-text nav-default">使用SNTP协议从时间服务器同步时间</span>
            <span class="next-text nav-mobile">下一篇</span>
            <i class="iconfont icon-right"></i>
          </a>
      </nav>
    </footer>
  </article>
        </div>
        

  <span id="/post/blog/network/0018-tun-example-for-setting-up-ip-tunnel/" class="leancloud_visitors" data-flag-title="使用tun虚拟网络接口建立IP隧道的实例">
		<span class="post-meta-item-text">文章阅读量 </span>
		<span class="leancloud-visitors-count">0</span>
		<p></p>
	  </span>
  <div id="vcomments"></div>
  <script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
  <script src='//unpkg.com/valine/dist/Valine.min.js'></script>
  <script type="text/javascript">
    new Valine({
        el: '#vcomments' ,
        appId: 'OFCGzCfJRUglzOdzrqMGkbTR-gzGzoHsz',
        appKey: 'v7P29kPAEbsmaavaYPNhGhnF',
        notify:  false ,
        verify:  false ,
        avatar:'mm',
        placeholder: '说点什么吧...',
        visitor:  true 
    });
  </script>

  

      </div>
    </main>

    <footer id="footer" class="footer">
      <div class="social-links">
      <a href="mailto:hengch@163.com" class="iconfont icon-email" title="email"></a>
  <a href="https://whowin.gitee.io/index.xml" type="application/rss+xml" class="iconfont icon-rss" title="rss"></a>
</div>
<div class="copyright">
  <span class="power-by">
    由 <a class="hexo-link" href="https://gohugo.io">Hugo</a> 强力驱动
  </span>
  <span class="division">|</span>
  <span class="theme-info">
    主题 - 
    <a class="theme-link" href="https://github.com/olOwOlo/hugo-theme-even">Even</a>
  </span>

  <div class="busuanzi-footer">
    <span id="busuanzi_container_site_pv"> 本站总访问量 <span id="busuanzi_value_site_pv"><img src="/img/spinner.svg" alt="spinner.svg"/></span> 次 </span>
      <span class="division">|</span>
    <span id="busuanzi_container_site_uv"> 本站总访客数 <span id="busuanzi_value_site_uv"><img src="/img/spinner.svg" alt="spinner.svg"/></span> 人 </span>
  </div>

  <span class="copyright-year">
    &copy; 
    2022 - 
    2024<span class="heart"><i class="iconfont icon-heart"></i></span><span>whowin</span>
  </span>
</div>

    </footer>

    <div class="back-to-top" id="back-to-top">
      <i class="iconfont icon-up"></i>
    </div>
  </div>
  
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/slideout@1.0.1/dist/slideout.min.js" integrity="sha256-t+zJ/g8/KXIJMjSVQdnibt4dlaDxc9zXr/9oNPeWqdg=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.1.20/dist/jquery.fancybox.min.js" integrity="sha256-XVLffZaxoWfGUEbdzuLi7pwaUJv1cecsQJQqGLe7axY=" crossorigin="anonymous"></script>



<script type="text/javascript" src="/js/main.min.64437849d125a2d603b3e71d6de5225d641a32d17168a58106e0b61852079683.js"></script>
  <script type="text/javascript">
    window.MathJax = {
      tex: {
        inlineMath: [['$','$'], ['\\(','\\)']],
        }
    };
  </script>
  <script async src="https://cdn.jsdelivr.net/npm/mathjax@3.0.5/es5/tex-mml-chtml.js" integrity="sha256-HGLuEfFcsUJGhvB8cQ8nr0gai9EucOOaIxFw7qxmd+w=" crossorigin="anonymous"></script>








</body>
</html>
